from typing import Any,Self,Tuple,Callable
from datetime import datetime

BOARD_LEN:int = 225
BOARD_SIZE:int = 15
P_BLACK:int = -1
P_EMPTY:int = 0
P_WHITE:int = 1
STARS = [
    (7,7),      # 天元
    (3,3),      # 左上
    (11,3),     # 右上
    (3,11),     # 左下
    (11,11),    # 右下
]

def hv_to_loc(h:int, v:int) -> int:
    return h + (v * BOARD_SIZE)
def loc_to_hv(loc:int) -> Tuple[int,int]:
    h = loc % BOARD_SIZE
    v = loc // BOARD_SIZE
    return (h,v)

def piece_not(p:int) -> int:
    return -p

class Board:
    def __init__(me, _from=None) -> None:
        if isinstance(_from, Board):
            me.__array = _from.__array
        else:
            me.__array = [P_EMPTY]*BOARD_LEN

    def __str__(me) -> str:
        return me.hash()

    def reset(me) -> Self:
        me.__array = [P_EMPTY]*BOARD_LEN
        return me

    def get(me, loc:int) -> int:
        if loc in range(BOARD_LEN):
            return me.__array[loc]
        return P_EMPTY

    def set(me, loc:int, p:int) -> Self:
        if loc in range(BOARD_LEN):
            me.__array[loc] = p
        return me

    def for_loc(me, f:Callable[[int,int],Any]) -> Self:
        for loc in range(BOARD_LEN):
            if not (f(loc, me.__array[loc]) is None): break
        return me

    def for_hv(me, f:Callable[[int,int,int],Any]) -> Self:
        for loc in range(BOARD_LEN):
            h,v = loc_to_hv(loc)
            if not (f(h, v, me.__array[loc]) is None): break
        return me

class Shape:
    D1 = 0              # -b-a--b-
    D2 = 1              # -b-aa-b-
    D3 = 2              # -b-aaab-
    D4 = 3              # -baaaab-
    S1 = 4              # -ba-----
    L1 = 5              # -a------
    S2 = 6              # -ba-a---
    L2 = 7              # -aa-----
    S3 = 8              # -b-aaa--
    L3 = 9              # -aaa----
    S4 = 10             # -baaaa--
    S44 = 11            # -a-aaaa-a-
    L4 = 12             # -aaaa---
    L5 = 13             # -aaaaa--
    LONG = 14           # -aaaaaa-
class _WAY:
    _H = (-1, 0, 1, 0)
    _V = (0, -1, 0, 1)
    _L = (-1, -1, 1, 1)
    _R = (-1, 1, 1, -1)
_SHAPE_MAP = {
    "aaaaaa": Shape.LONG,
    #
    "aaaaab": Shape.L5,
    "aaaaa-": Shape.L5,
    "baaaaa": Shape.L5,
    "-aaaaa": Shape.L5,
    #
    "-aaaa-": Shape.L4,
    #
    "aaa-ab": Shape.S4,
    "aa-aab": Shape.S4,
    "a-aaab": Shape.S4,
    "aaa-a-": Shape.S4,
    "aa-aa-": Shape.S4,
    "a-aaa-": Shape.S4,
    "baaa-a": Shape.S4,
    "baa-aa": Shape.S4,
    "ba-aaa": Shape.S4,
    "baaaa-": Shape.S4,
    "-aaaab": Shape.S4,
    #
    "baaaab": Shape.D4,
    #
    "-aaa-b": Shape.L3,
    "b-aaa-": Shape.L3,
    "-aaa--": Shape.L3,
    "-aa-a-": Shape.L3,
    "-a-aa-": Shape.L3,
    "--aaa-": Shape.L3,
    #
    "a-a-ab": Shape.S3,
    "a--aab": Shape.S3,
    "aaa---": Shape.S3,
    "aa-a--": Shape.S3,
    "aa--a-": Shape.S3,
    "a-a-a-": Shape.S3,
    "a--aa-": Shape.S3,
    "baaa--": Shape.S3,
    "ba-aa-": Shape.S3,
    "ba-a-a": Shape.S3,
    "-aa--a": Shape.S3,
    "-aa-ab": Shape.S3,
    "-a-aab": Shape.S3,
    "-a-a-a": Shape.S3,
    "-a--aa": Shape.S3,
    "--a-aa": Shape.S3,
    "---aaa": Shape.S3,
    "--aaab": Shape.S3,
    #
    "baaa-b": Shape.D3,
    "baa-ab": Shape.D3,
    "ba-aab": Shape.D3,
    "b-aaab": Shape.D3,
    #
    "b-aa--": Shape.L2,
    "b-a-a-": Shape.L2,
    "b--aa-": Shape.L2,
    "-aa--b": Shape.L2,
    "-a-a-b": Shape.L2,
    "--aa-b": Shape.L2,
    "-aa---": Shape.L2,
    "-a-a--": Shape.L2,
    "-a--a-": Shape.L2,
    "--aa--": Shape.L2,
    "--a-a-": Shape.L2,
    "---aa-": Shape.L2,
    #
    "a---ab": Shape.S2,
    "baa---": Shape.S2,
    "ba-a--": Shape.S2,
    "ba--a-": Shape.S2,
    "ba---a": Shape.S2,
    "-a--ab": Shape.S2,
    "--a-ab": Shape.S2,
    "---aab": Shape.S2,
    #
    "baa--b": Shape.D2,
    "ba-a-b": Shape.D2,
    "ba--ab": Shape.D2,
    "b-a-ab": Shape.D2,
    "b--aab": Shape.D2,
    "b-aa-b": Shape.D2,
    #
    "b-a---": Shape.L1,
    "b--a--": Shape.L1,
    "b---a-": Shape.L1,
    "-a---b": Shape.L1,
    "--a--b": Shape.L1,
    "---a-b": Shape.L1,
    "-a----": Shape.L1,
    "--a---": Shape.L1,
    "---a--": Shape.L1,
    "----a-": Shape.L1,
    #
    "ba----": Shape.S1,
    "b-a---": Shape.S1,
    "b--a--": Shape.S1,
    "b---a-": Shape.S1,
    "b----a": Shape.S1,
    "a----b": Shape.S1,
    "-a---b": Shape.S1,
    "--a--b": Shape.S1,
    "---a-b": Shape.S1,
    "----ab": Shape.S1,
    #
    "ba---b": Shape.D1,
    "b-a--b": Shape.D1,
    "b--a-b": Shape.D1,
    "b---ab": Shape.D1,
}
def shape_for_point(board:Board, piece:int, loc:int, way:tuple) -> int:
    def a_point(h:int, v:int) -> Tuple[int,int,int]:
        a_h,a_v = h,v
        diff_hv = 1
        while (h in range(BOARD_SIZE)) and (v in range(BOARD_SIZE)) and (diff_hv > -5):
            p = board.get(hv_to_loc(h,v))
            a_h,a_v = h,v
            if (p == P_EMPTY) or (p == piece):
                diff_hv -= 1
                h += way[0]
                v += way[1]
            else: break
        return (a_h, a_v, diff_hv)
    def way_b_to_parr(h:int, v:int) -> Tuple[list,bool]:
        parr = ['b', 'b', 'b', 'b', 'b', 'b']
        hit_side = False
        for i in range(6):
            if (h in range(BOARD_SIZE)) and (v in range(BOARD_SIZE)):
                p = board.get(hv_to_loc(h,v))
                if (p == piece): parr[i] = 'a'
                elif (p == P_EMPTY): parr[i] = '-'
                else: hit_side = True
                h += way[2]
                v += way[3]
            else: break
        return (parr,hit_side)
    max_shape = Shape.D1
    n_s4 = 0
    hv_mid = loc_to_hv(loc)
    h,v,diff_hv = a_point(*hv_mid)
    while (h in range(BOARD_SIZE)) and (v in range(BOARD_SIZE)) and (diff_hv < 1):
        now_parr,hit_side = way_b_to_parr(h,v)
        now_shape = _SHAPE_MAP.get((''.join(now_parr)), Shape.D1)
        if now_shape > max_shape: max_shape = now_shape
        #
        if now_shape == Shape.S4: n_s4 += 1
        #
        if hit_side: break
        h += way[2]
        v += way[3]
        diff_hv += 1
    if n_s4 > 1: return Shape.S44
    return max_shape

def shape_line4(board:Board, piece:int, loc:int) -> Tuple[int,int,int,int]:
    line4 = (shape_for_point(board, piece, loc, _WAY._H),
             shape_for_point(board, piece, loc, _WAY._V),
             shape_for_point(board, piece, loc, _WAY._L),
             shape_for_point(board, piece, loc, _WAY._R))
    return line4

def _check_forbidden(board:Board, loc:int) -> bool:
    return False

class RuleTest:
    NORMAL_MOVE = 0
    OVER_BY_WIN = 1
    OVER_BY_WINLONG = 2
    OVER_BY_FORBIDDEN = 3
class RuleBase:
    def __init__(me, uid:str, use_forbidden:bool, swap_steps:list=None, select_step:int=None, select_nums:int=None):
        me.__uid = uid
        me.__use_forbidden = use_forbidden
        me.__swap_steps = swap_steps if isinstance(swap_steps,list) else []
        me.__select_step = select_step if (select_step in range(1,BOARD_LEN)) else -1
        me.__select_nums = select_nums if isinstance(select_nums,int) else 0

    def uid(me) -> str: return me.__uid

    def use_forbidden(me) -> str: return me.__use_forbidden

    def can_swap(me, now_step:int) -> bool:
        return (now_step in me.__swap_steps)

    def need_give_select(me, now_step:int) -> bool:
        return (now_step == me.__select_step)

    def select_nums(me) -> int: return me.__select_nums

    def test_move(me, board:Board, piece:int, loc:int) -> int:
        shapes = shape_line4(board, piece, loc)
        n_long = 0
        for shape in shapes:
            if shape == Shape.L5: return RuleTest.OVER_BY_WIN
            elif shape == Shape.LONG: n_long += 1
        if (n_long > 0) and ((piece == P_WHITE) or (not me.__use_forbidden)):
            return RuleTest.OVER_BY_WINLONG
        if me.__use_forbidden and (piece == P_BLACK) and _check_forbidden(board, loc):
            return RuleTest.OVER_BY_FORBIDDEN
        return RuleTest.NORMAL_MOVE

class Rules:
    _all = []
    @classmethod
    def _init_by_main_use_only(obj):
        obj._all = [
            RuleBase("gomoku", use_forbidden=False),
            RuleBase("1swap", use_forbidden=False, swap_steps=[1]),
            RuleBase("forbidden", use_forbidden=True),
        ]
    @classmethod
    def all(obj) -> list:
        return obj._all.copy()
    @classmethod
    def by_uid(obj, uid:str) -> RuleBase:
        for rule in obj._all:
            if rule.uid() == uid:
                return rule
        return None
    @classmethod
    def std_second_alls(_) -> tuple:
        return (0, 600, 1800, 3600, 5400, 7200)
    @classmethod
    def std_second_steps(_) -> tuple:
        return (0, 60, 300, 600, 1200, 1800)

class MoveStep:
    def __init__(me, step:int, loc:int, p:int) -> None:
        me.__step = step
        me.__loc = loc
        me.__p = p

    def step(me) -> int: return me.__step
    def loc(me) -> int: return me.__loc
    def piece(me) -> int: return me.__p

class GameOver:
    UNOVER = 0
    NORMAL = 1
    LONG = 2
    FORBIDDEN = 3
    DRAW = 4
    OVERTIME = 5
class Game:
    def __init__(me, rule:RuleBase) -> None:
        me.__rule = rule
        me.reset()

    def reset(me) -> Self:
        me.__next_piece = P_BLACK
        me.__next_step = 1
        me.__piece_nums = 0
        me.__over_type = GameOver.UNOVER
        me.__record = []
        me.__undos = []
        me.__board = Board()
        me.__updatetime = datetime.now()
        return me

    def change_rule(me, rule_to:RuleBase) -> Self:
        me.__rule = rule_to
        return me

    def rule(me) -> RuleBase: return me.__rule

    def updatetime(me) -> datetime: return me.__updatetime

    def next_piece(me) -> int: return me.__next_piece

    def next_step(me) -> int: return me.__next_step

    def board(me) -> Board: return me.__board

    def has_over(me) -> bool: return (me.__over_type != GameOver.UNOVER)

    def over_type(me) -> int: return me.__over_type

    def step_nums(me) -> int: return len(me.__record)

    def step_record(me, i:int) -> MoveStep|None:
        if i in range(len(me.__record)):
            return me.__record[i]
        return None

    def last(me, i:int=1) -> MoveStep|None:
        if i in range(1, len(me.__record)+1):
            return me.__record[-i]
        return None

    def canmove(me, h:int, v:int) -> int:
        if me.has_over(): return False
        if not ((h in range(BOARD_SIZE)) and (v in range(BOARD_SIZE))): return False
        if me.__board.get(hv_to_loc(h,v)) != P_EMPTY: return False
        return True

    def move(me, h:int, v:int) -> Self:
        if me.canmove(h,v):
            loc = hv_to_loc(h,v)
            me.__undos.clear()
            me.__record.append(MoveStep(me.__next_step, loc, me.__next_piece))
            me.__board.set(loc, me.__next_piece)
            rt = me.__rule.test_move(me.__board, me.__next_piece, loc)
            if rt == RuleTest.OVER_BY_FORBIDDEN:
                me.__over_type = GameOver.FORBIDDEN
            elif rt == RuleTest.OVER_BY_WIN:
                me.__over_type = GameOver.NORMAL
            elif rt == RuleTest.OVER_BY_WINLONG:
                me.__over_type = GameOver.LONG
            me.__next_piece = piece_not(me.__next_piece)
            me.__next_step += 1
            me.__piece_nums += 1
            if me.__piece_nums >= BOARD_LEN:
                me.__over_type = GameOver.DRAW
            me.__updatetime = datetime.now()
        return me

    def undo(me) -> Self:
        if len(me.__record) > 0:
            ms = me.__record.pop()
            me.__undos.append(ms)
            me.__board.set(ms.loc(), P_EMPTY)
            me.__next_piece = ms.piece()
            me.__next_step = ms.step()
            me.__piece_nums -= 1
            me.__updatetime = datetime.now()
        return me

    def redo(me) -> Self:
        if len(me.__undos) > 0:
            ms = me.__undos.pop()
            me.__record.append(ms)
            me.__board.set(ms.loc(), ms.piece())
            me.__next_piece = piece_not(ms.piece())
            me.__next_step += 1
            me.__piece_nums += 1
            me.__updatetime = datetime.now()
        return me

    def now_piece_overtime(me) -> Self:
        me.__over_type = GameOver.OVERTIME
        me.__updatetime = datetime.now()
        return me
